package de.dhbw.karlsruhe.picsim.model.cpu.execute;

import de.dhbw.karlsruhe.picsim.model.actions.*;
import de.dhbw.karlsruhe.picsim.model.cpu.FileMutable;
import de.dhbw.karlsruhe.picsim.model.cpu.InstructionPipeline;
import de.dhbw.karlsruhe.picsim.model.cpu.ProgramCounterMutable;
import de.dhbw.karlsruhe.picsim.model.memory.*;
import de.dhbw.karlsruhe.picsim.model.memory.impl.CommandRegisterAddressImpl;
import de.dhbw.karlsruhe.picsim.model.memory.impl.FileAddressImpl;
import de.dhbw.karlsruhe.picsim.model.memory.impl.RegisterValueImpl;
import de.dhbw.karlsruhe.picsim.model.timer.TimerFacade;
import de.dhbw.karlsruhe.picsim.util.Bits;
import de.dhbw.karlsruhe.picsim.util.BitsMutable;
import de.dhbw.karlsruhe.picsim.util.impl.BitUtilities;
import de.dhbw.karlsruhe.picsim.util.impl.BitsMutableImpl;
import de.dhbw.karlsruhe.picsim.util.impl.Lengths;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Created by IntelliJ IDEA.
 * User: goesen
 * Date: Apr 14, 2010
 * Time: 10:06:01 AM
 */
public class ActionExecutorImpl implements ActionExecutor {
    private static final Log log = LogFactory.getLog(ActionExecutorImpl.class);
    private final FileMemoryMutable fileMemory;
    private final FileMutable wRegister;
    private final StackFacade stackFacade;
    private final ProgramCounterMutable programCounter;
    private final HiddenRegisters hidden;
    private final InstructionPipeline instructionPipeline;
    private final TimerFacade timerFacade;


    public ActionExecutorImpl(final FileMemoryMutable fileMemory, final FileMutable wRegister,
                              final StackFacade stackFacade, final ProgramCounterMutable programCounter,
                              final HiddenRegisters hidden, final InstructionPipeline instructionPipeline,
                              final TimerFacade timerFacade) {

        this.fileMemory = fileMemory;
        this.wRegister = wRegister;
        this.stackFacade = stackFacade;
        this.programCounter = programCounter;
        this.hidden = hidden;
        this.instructionPipeline = instructionPipeline;
        this.timerFacade = timerFacade;
    }

    @Override
    public void executeActions(@NotNull final List<? extends PicAction> picActions) {
        final AtomicBoolean programCounterWasModified = new AtomicBoolean(false);
        for (final PicAction action : picActions) {
            action.acceptVisitor(new ActionVisitor(programCounterWasModified));
        }
        if (programCounterWasModified.get()) {
            log.info("the program counter was modified. Pic needs an extra noOp to complete command");
            instructionPipeline.addExtraNoOp();
        }
    }

    private void handleWriteInFile(final FileAddress bankedAddress, final RegisterValue value,
                                   final AtomicBoolean programCounterWasModified) {
        final FileMutable targetFile = fileMemory.getFile(bankedAddress);

        log.debug("Command wants to write " + value.asInteger() + " to " + BitUtilities.fromLeastSignificantBitFirst(
                bankedAddress.getBits()));

        doWrite(targetFile, value);

        if (targetFile.equals(fileMemory.getFile(FileAddressImpl.programCounterLowAddress()))) {
            log.debug("write to pcl detected");
            // transferiere die bits <0,4> aus pcLath nach pcHigh
            final Bits latchValue = fileMemory.getFile(FileAddressImpl.programCounterLatch()).getData();
            hidden.setPcHighBits(BitUtilities.subBits(latchValue, 0, 5));
            programCounterWasModified.set(true);
        } else if (targetFile.equals(fileMemory.getFile(FileAddressImpl.timerAddress()))) {
            //benachrichtige die Timer Facade um optional den Prescaler benachrichtigen zu können
            timerFacade.writeToTimerRegister();
        }
    }

    private FileAddress getBankedAddress(final WriteInFileAction action) {
        final FileMutable statusFile = fileMemory.getFile(FileAddressImpl.statusAddress());
        final Bits bits = statusFile.getData();

        //erzeuge die gebankte Adresse
        final Bits address = action.getTargetAddress();
        final List<Boolean> isBank1 = Collections.singletonList(bits.isSet(5));
        final BitsMutable isBank1Bit = new BitsMutableImpl(isBank1);
        final Bits newBits = BitUtilities.concat(address, isBank1Bit);
        return new FileAddressImpl(newBits);
    }

    private void doWrite(final FileMutable targetFile, final RegisterValue value) {


        targetFile.setValue(value);
    }

    private class ActionVisitor implements PicAction.Visitor {
        private final AtomicBoolean programCounterWasModified;

        private ActionVisitor(final AtomicBoolean programCounterWasModified) {
            this.programCounterWasModified = programCounterWasModified;
        }

        @Override
        public void visit(@NotNull final WriteInFileAction action) {
            final FileAddress bankedAddress = getBankedAddress(action);
            final RegisterValue value = action.getValue();
            handleWriteInFile(bankedAddress, value, programCounterWasModified);
        }

        @Override
        public void visit(@NotNull final WriteInWAction action) {
            final RegisterValue newValue = action.getValue();
            log.debug("overwriting oldValue " + wRegister.getData() + " with " + newValue);
            wRegister.setValue(newValue);
        }

        @Override
        public void visit(@NotNull final StatusAction action) {
            final FileMutable fileMutable = fileMemory.getFile(FileAddressImpl.statusAddress());

            final BitsMutable newBits = new BitsMutableImpl(fileMutable.getData());
            newBits.setBit(action.getAffectedStatus().getPosition(), action.getNewValue());
            final RegisterValue newValue = new RegisterValueImpl(newBits);
            fileMutable.setValue(newValue);
        }

        @Override
        public void visit(@NotNull final PushToStackAction action) {
            final CommandRegisterAddress currentValue = programCounter.getCurrentValue();
            stackFacade.pushAddress(currentValue);
        }

        @Override
        public void visit(@NotNull final PopFromStackAction action) {
            final CommandRegisterAddress address = stackFacade.popAddress();
            log.debug(address + " was poped from stack");
            programCounter.setCurrentValue(address);
            programCounterWasModified.set(true);
        }

        @Override
        public void visit(@NotNull final ModifyProgramCounterAction action) {
            programCounterWasModified.set(true);
            if (action.getLowBits().getLength() == 0) {
                //dann ist es ein bittest-skip Befehl der den nächsten Befehl überspringen will
                //da der programCounter bereits wärend dem dekodieren verändert wurde, werhöhen wir ihn einfach nochmal
                programCounter.increment();
            } else if (action.getLowBits().getLength() == Lengths.CALL_GOTO_ARGUMENT_LENGTH) {
                /*ist es ein Call oder Goto
                 es ist notwendig das pcHighByte zu schreiben

                 P 17
                 The PCLATH (PC latch high) register is a holding register for PC<12:8>.
                 The contents oPCLATH are transferred to the upper byte of the program counter when the PC is
                 loaded with a new value. This occurs during a CALL, GOTO or a write to PCL.
                */
                final FileMutable pcLath = fileMemory.getFile(FileAddressImpl.programCounterLatch());

                // transferiere die bits 3,4 vom latch ins pc high

                final Bits latchBits = BitUtilities.subBits(pcLath.getData(), 3, 5);

                programCounter.setCurrentValue(
                        new CommandRegisterAddressImpl(BitUtilities.concat(action.getLowBits(), latchBits)));

            } else {
                //ansonten ist es etwas unbekanntes
                throw new IllegalArgumentException(
                        action.toString() + " has illegal bitLength " + action.getLowBits().getLength());
            }
        }

        @Override
        public void visit(@NotNull final ClearWatchDogAction clearWatchDogAction) {
            timerFacade.clearWatchdog();
        }
    }
}
